package qmlt.dataset.filter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONException;

import qmlt.dataset.Attribute;
import qmlt.dataset.DataSetBase;
import qmlt.dataset.DataSet;
import qmlt.dataset.DataSetDef;
import qmlt.dataset.Instance;
import qmlt.test.TestDataSet;

/**
 * This filter will convert all the categorical feature values into numeric ones, in a 1-to-N
 * mapping manner. However, it does not touch the target values.
 * 
 * @author quyin
 * 
 */
public class CategoricalToNumericFilter implements Filter
{
	
	public static double	defaultPosValue	= 0.9;

	public static double	defaultNegValue	= 0.1;

	public double					posValue;

	public double					negValue;

	public CategoricalToNumericFilter()
	{
		this(defaultPosValue, defaultNegValue);
	}

	public CategoricalToNumericFilter(double posValue, double negValue)
	{
		this.posValue = posValue;
		this.negValue = negValue;
	}

	@Override
	public DataSet filter(DataSet ds)
	{
		Map<Attribute, List<Object>> allValidValues = new HashMap<Attribute, List<Object>>();
		List<Attribute> newFeatureDefs = processFeatureDefs(ds, allValidValues);
		DataSetDef newDef = new DataSetDef(ds.getId() + "-ctn", newFeatureDefs, ds.getTargetDef());
		DataSet newDs = new DataSetBase(newDef, ds);

		for (Instance inst : ds.getInstances())
		{
			Instance newInst = new Instance(inst.id, newDs);
			newInst.setFeatures(processData(allValidValues, inst));
			newInst.setTarget(inst.getTarget());
			newDs.getInstances().add(newInst);
		}

		return newDs;
	}

	private List<Object> processData(Map<Attribute, List<Object>> allValidValues, Instance instance)
	{
		List<Object> newInstData = new ArrayList<Object>();
		
		List<Attribute> featureDefs = instance.dataSet.getFeatureDefs();
		List<Object> data = instance.getFeatures();
		
		for (int i = 0; i < featureDefs.size(); ++i)
		{
			Attribute a = featureDefs.get(i);
			Object v = data.get(i);

			if (a.type.equals(Attribute.FLOAT))
			{
				newInstData.add(v);
			}
			else
			{
				for (Object vv : allValidValues.get(a))
				{
					if (vv.equals(v))
					{
						newInstData.add((float) posValue);
					}
					else
					{
						newInstData.add((float) negValue);
					}
				}
			}
		}
		
		return newInstData;
	}

	private List<Attribute> processFeatureDefs(DataSet ds, Map<Attribute, List<Object>> allValidValues)
	{
		List<Attribute> featureDefs = ds.getFeatureDefs();
		List<Attribute> newFeatureDefs = new ArrayList<Attribute>();
		
		for (int i = 0; i < featureDefs.size(); ++i)
		{
			Attribute a = featureDefs.get(i);
			if (a.type.equals(Attribute.FLOAT))
			{
				newFeatureDefs.add(a);
			}
			else // a.type.equals(Attribute.STRING)
			{
				List<Object> validValues = getFeatureValidValues(ds, i);
				allValidValues.put(a, validValues);
				for (Object v : validValues)
				{
					Attribute b = new Attribute(Attribute.FLOAT, null);
					b.validValues = validValues;
					newFeatureDefs.add(b);
				}
			}
		}
		
		return newFeatureDefs;
	}

	private List<Object> getFeatureValidValues(DataSet ds, int i)
	{
		List<Object> rst = new ArrayList<Object>();
		for (Instance inst : ds.getInstances())
		{
			if (!rst.contains(inst.getFeatures().get(i)))
			{
				rst.add(inst.getFeatures().get(i));
			}
		}
		return rst;
	}

	public static void main(String[] args) throws IOException, JSONException
	{
		DataSet ds = DataSetBase.read("data/house-votes-84.ds", "data/house-votes-84.data");
		TestDataSet.printDataSet(ds);
		Filter f = new CategoricalToNumericFilter();
		DataSet nds = f.filter(ds);
		TestDataSet.printDataSet(nds);
	}
}
